package com.dries.portal.module.coupon.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.dries.common.constant.Constant;
import com.dries.portal.module.coupon.enums.CouponStatus;
import com.dries.common.exception.ApiException;
import com.dries.portal.module.coupon.service.IRedisService;
import com.dries.portal.module.coupon.vo.CouponVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @Description: Redis相关的操作服务接口实现
 * @Author ZSY
 * @createTime 2020/7/29 14:15
 */
@Slf4j
@Service
public class CouponRedisServiceImpl implements IRedisService {

    @Resource
    private StringRedisTemplate couPonRedisTemplate;

    /**
     * 根据userId 和状态 找缓存的优惠券列表数据
     *
     * @param userId
     * @param status
     */
    @Override
    public List<CouponVo> getCacheCoupons(Long userId, Integer status) {

        String redisKey = status2RedisKey(status, userId);
        List<String> values = couPonRedisTemplate.opsForHash().values(redisKey)
                .stream().map(e -> Objects.toString(e, null))
                .collect(Collectors.toList());

        if (values.size() == 1 && JSONUtil.toBean(values.get(0), CouponVo.class).getId() == -1)
            return Collections.emptyList();

        if (CollectionUtil.isEmpty(values)) {
            saveEmptyCouponListToCache(userId , Collections.singletonList(status));
            return Collections.emptyList();
        }

        return values.stream()
                .map(e -> JSONUtil.toBean(e, CouponVo.class))
                .collect(Collectors.toList());
    }

    /**
     * 避免缓存穿透 保存空的优惠券列表到缓存中
     *
     * @param userId
     * @param status
     */
    @Override
    public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {

        log.info("保存空的用户缓存，用户：{},状态： {}", userId, JSONUtil.toJsonStr(status));
        Map<String, String> invalidCouponMap = new HashMap<>();
        invalidCouponMap.put("-1", JSONUtil.toJsonStr(CouponVo.invalidCoupon()));

        // 使用 SessionCallback 把数据命令放入到 Redis 的 pipeline
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                status.forEach(s -> {
                    String redisKey = status2RedisKey(s, userId);
                    redisOperations.opsForHash().putAll(redisKey, invalidCouponMap);
                });
                return null;
            }
        };
        List<Object> objects = couPonRedisTemplate.executePipelined(sessionCallback);
        log.info("Pipeline 执行结果: {}", JSONUtil.toJsonStr(objects));
    }

    /**
     * 尝试从 Cache 中获取一个优惠券码
     *
     * @param templateId
     */
    @Override
    public String tryToAcquireCouponCodeFromCache(Integer templateId) {

        String redisKey = String.format("%s%s", Constant.RedisPrefix.COUPON_TEMPLATE, templateId);
        // 优惠券码不存在顺序关系，左边 pop 或右边 pop 没有影响
        String leftPop = couPonRedisTemplate.opsForList().leftPop(redisKey);
        log.info("========》 {}", leftPop);
        return leftPop;
    }

    /**
     * 将优惠券保存到cache中
     *
     * @param userId
     * @param coupons
     * @param status
     */
    @Override
    public Integer addCouponToCache(Long userId, List<CouponVo> coupons, Integer status) {

        log.info("添加优惠券信息： {},{},{}",
                userId, JSONUtil.toJsonStr(coupons), status);

        Integer result = -1;
        CouponStatus couponStatus = CouponStatus.of(status);
        switch (couponStatus) {
            case USABLE:
                addCouponToCacheForUsab(userId, coupons);
                break;
            case USED:
                addCouponToCacheForUsed(userId, coupons);
                break;
            case EXPIRED:
                addCouponToCacheForExpired(userId,coupons);
                break;
        }

        return result;
    }

    /**
     * 新增优惠券到 Cache中
     */
    @SuppressWarnings("all")
    private Integer addCouponToCacheForUsab(Long userId, List<CouponVo> coupons) {
        // 如果 status 是 USABLE,代表是新增的优惠券
        // 只会影响一个Cache： USER_COUPON_USABLE
        Map<String, String> needCachedObject = new HashMap<>();

        coupons.forEach(e -> {
            needCachedObject.put(e.getId().toString(), JSONUtil.toJsonStr(e));
        });

        String redisKey = status2RedisKey(CouponStatus.USABLE.getCode(), userId);
        couPonRedisTemplate.opsForHash().putAll(redisKey, needCachedObject);

        couPonRedisTemplate.expire(
                redisKey,
                getRandomExpirationTime(1,2),
                TimeUnit.SECONDS
        );
        return needCachedObject.size();
    }
    /**
     * 将已使用的优惠券加入到缓存中
     */
    @SuppressWarnings("all")
    private Integer addCouponToCacheForUsed(Long userId, List<CouponVo> coupons) {
        // 如果 status 是 USED,代表是使用当前的优惠券
        // 影响两个Cache： USER_COUPON_USABLE， USER_COUPON_USED
        Map<String, String> needCachedForUsed = new HashMap<>(coupons.size());
        String redisKeyForUsable = status2RedisKey(CouponStatus.USABLE.getCode(),userId);
        String redisKeyForUsed = status2RedisKey(CouponStatus.USED.getCode(), userId);

        // 获取当前用户可用的优惠券
        List<CouponVo> curUsableCoupons = getCacheCoupons(userId, CouponStatus.USABLE.getCode());
        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > coupons.size();

        coupons.forEach(e -> {
            needCachedForUsed.put(e.getId().toString(), JSONUtil.toJsonStr(e));
        });
        // 校验当前的优惠券参数是否与 Cached 中的匹配
        List<Integer> curUsableIds = curUsableCoupons.stream().map(CouponVo::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream().map(CouponVo::getId).collect(Collectors.toList());
        // paramIds是否curUsableIds集合子集，paramIds集合大小<=curUsableIds集合大小
        if (!CollectionUtils.containsAny(paramIds, curUsableIds)) {
            throw new ApiException("优惠券参数错误");
        }
        List<String> needCleanKey = paramIds.stream()
                .map(Object::toString).collect(Collectors.toList());
        // 使用 SessionCallback 把数据命令放入到 Redis 的 pipeline
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 1. 已使用的优惠券 缓存添加
                redisOperations.opsForHash().putAll(redisKeyForUsed, needCachedForUsed);

                // 2. 可用的优惠券 缓存清理
                redisOperations.opsForHash().delete(redisKeyForUsable, needCleanKey.toArray());

                // 3. 重置过期时间
                redisOperations.expire(redisKeyForUsed,
                        getRandomExpirationTime(1,2),
                        TimeUnit.SECONDS
                        );
                redisOperations.expire(redisKeyForUsable,
                        getRandomExpirationTime(1,2),
                        TimeUnit.SECONDS);
                return null;
            }
        };
        log.info("Pipeline 执行结果: {}",
                JSONUtil.toJsonStr(couPonRedisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }
    /**
     * 将已过期的优惠券加入到 缓存中
     */
    @SuppressWarnings("all")
    private Integer addCouponToCacheForExpired(Long userId, List<CouponVo> coupons) {
        // 如果 status 是 EXPIRED,代表是使用当前的优惠券已过期
        // 影响两个Cache： USER_COUPON_USABLE， USER_COUPON_EXPIRED

        // 最终需要保存的 Cache
        Map<String, String> needCachedExpired = new HashMap<>(coupons.size());

        String redisKeyForUsable = status2RedisKey(CouponStatus.USABLE.getCode(), userId);
        String redisKeyForExpired = status2RedisKey(CouponStatus.EXPIRED.getCode(), userId);

        List<CouponVo> curUsableCoupons = getCacheCoupons(userId, CouponStatus.USABLE.getCode());
        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > coupons.size();
        // 校验当前的优惠券参数是否与 Cached 中的匹配
        coupons.forEach(e -> needCachedExpired.put(e.getId().toString(), JSONUtil.toJsonStr(e)));
        List<Integer> curUsableIds = curUsableCoupons.stream().map(CouponVo::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream().map(CouponVo::getId).collect(Collectors.toList());
        if (!CollectionUtils.containsAny(paramIds, curUsableIds)) {
            throw new ApiException("优惠券参数错误");
        }
        List<String> needCleanKey = paramIds.stream()
                .map(Object::toString).collect(Collectors.toList());
        // 使用 SessionCallback 把数据命令放入到 Redis 的 pipeline
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations redisOperations) throws DataAccessException {
                // 1. 已过期的优惠券 缓存添加
                redisOperations.opsForHash().putAll(redisKeyForExpired, needCachedExpired);
                // 2. 可用的优惠券 缓存清理
                redisOperations.opsForHash().delete(redisKeyForUsable, needCleanKey.toArray());
                // 3. 重置过期时间
                redisOperations.expire(redisKeyForUsable, getRandomExpirationTime(1,2), TimeUnit.SECONDS);
                redisOperations.expire(redisKeyForExpired, getRandomExpirationTime(1,2), TimeUnit.SECONDS);
                return null;
            }
        };
        log.info("Pipeline 执行结果: {}",
                JSONUtil.toJsonStr(couPonRedisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }

    // 根据 status 获取到对应的 Redis Key
    private String status2RedisKey(Integer status, Long userId) {
        String redisKey = null;
        CouponStatus couponStatus = CouponStatus.of(status);

        switch (couponStatus) {
            case USABLE:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USABLE, userId);
                break;
            case USED:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USED, userId);
                break;
            case EXPIRED:
                redisKey = String.format("%s%s", Constant.RedisPrefix.USER_COUPON_EXPIRED, userId);
                break;
        }

        return redisKey;
    }

    /**
     * 获取一个随机的过期时间
     * 缓存雪崩： key在同一时间失效
     * @param min
     * @param max
     * @return
     */
    private Long getRandomExpirationTime(Integer min, Integer max) {
//        RandomUtils.nextLong();
        return RandomUtil.randomLong(
                min * 60 * 60,
                max * 60 * 60);
    }
}
